package game;
import java.util.LinkedList;
import java.util.Set;

import exceptions.AlreadyOccupiedException;
import exceptions.GameOverException;
import exceptions.IllegalMoveException;
import exceptions.ImpossibleMoveException;
import exceptions.KoViolationException;
import exceptions.NotOccupiedException;
import exceptions.SuicideMoveException;
import exceptions.SuperKoViolationException;
import exceptions.UnknownColorException;


public class GoGame {

	float komi;
	GoBoard board;
	int next_group_id;
	int ko_position;
	int ko_last_move;
	int ko_last_stone;
	int ko_last_take;
	LinkedList<Integer> moves;
	LinkedList<GoGroup> groups_list;
	GoGroup[] groups_by_intersection;
	int color_to_play;
	int black_prisoners; // Number of black stones captured
	int white_prisoners; // Number of white stones captured
	LinkedList<Long> hash_list;
	final static public int BLACK = GoBoard.BLACK;
	final static public int WHITE = GoBoard.WHITE;

	public GoGame(int size, float komi) {
		this.komi = komi;
		this.ko_position = -1;
		this.ko_last_move = -1;
		this.ko_last_take = -1;
		this.ko_last_stone = -1;
		this.board = new GoBoard(size);
		this.moves = new LinkedList<Integer>();
		next_group_id = 0;
		groups_list= new LinkedList<GoGroup>();
		groups_by_intersection = new GoGroup[size*size];
		color_to_play = BLACK;
		black_prisoners = 0;
		white_prisoners = 0;
		hash_list = new LinkedList<Long>();
		for (int i = 0 ; i < 7 ; i++) {
			hash_list.add((long) 0);
		}
	}
	
	public GoGame(GoGame a) {
		this.komi = a.komi;
		this.board = new GoBoard(a.board);
		this.next_group_id = a.next_group_id;
		this.ko_position = a.ko_position;
		this.ko_last_move = a.ko_last_move;
		this.ko_last_stone = a.ko_last_stone;
		this.ko_last_take = a.ko_last_take;
		this.moves = (LinkedList<Integer>) a.moves.clone();
		this.groups_list = new LinkedList<GoGroup>();
		int size = board.getSize();
		this.groups_by_intersection = new GoGroup[size*size];
		for (GoGroup group : a.getGroupsList()) {
			GoGroup new_group = new GoGroup(group,this);
			this.groups_list.add(new_group);
			for (int stone : group.getStonesPositions()) {
				this.groups_by_intersection[stone] = new_group;
			}
		}
		this.color_to_play = a.color_to_play;
		this.black_prisoners = a.black_prisoners;
		this.white_prisoners = a.white_prisoners;
		this.hash_list = new LinkedList<Long>();
		this.hash_list.addAll(a.hash_list);
	}

	public float getKomi() {
		return komi;
	}
	
	public GoBoard getBoard() {
		return board;
	}
	
	public int getLastGroupId() {
		return next_group_id;
	}
	
	public LinkedList<Integer> getPlayedMoves() {
		return moves;
	}
	
	public LinkedList<GoGroup> getGroupsList() {
		return groups_list;
	}
	
	public GoGroup[] getGroupsByIntersection() {
		return groups_by_intersection;
	}
	
	public int getBlackPrisoners() {
		return this.black_prisoners;
	}
	
	public int getWhitePrisoners() {
		return this.white_prisoners;
	}
	
	public int getColorToPlay() {
		return this.color_to_play;
	}
	
	public int getNewGroupId() {
		next_group_id ++;
		return next_group_id;
	}
	
	public int getKoLastStone() {
		return this.ko_last_stone;
	}
	
	public GoGroup getIntersectionGroup(int position) throws NotOccupiedException, ImpossibleMoveException {
		if (board.isFree(position)) throw new NotOccupiedException(board, position);
		return groups_by_intersection[position];
	}
	
	public void move(int x, int y) throws AlreadyOccupiedException, SuicideMoveException, ImpossibleMoveException, KoViolationException, GameOverException {
		move(x*board.getSize()+y);
	}

	public void move(int position) throws AlreadyOccupiedException, ImpossibleMoveException, SuicideMoveException, KoViolationException, GameOverException  {
		int move_color = color_to_play;
		int opponent_color;
		try {
			opponent_color = GoBoard.reverseColor(move_color);
		} catch (UnknownColorException e1) {
			System.err.println("Internal error");
			e1.printStackTrace();
			return;
		}
		
		//Is it a passing move ?
		if (position == GoBoard.PASSING_MOVE) {
			if (move_color == BLACK) this.black_prisoners++;
			if (move_color == WHITE) {
				if (moves.getLast() == GoBoard.PASSING_MOVE) {
					// The two players have passed consecutively, and black passed first : we end the game
					GameOverException e = new GameOverException(this.isBlackWinning());
					e.setSequence(this.moves);
					throw e;
				} else this.white_prisoners++;
			}
			moves.add(GoBoard.PASSING_MOVE);
			this.color_to_play = opponent_color;
			return;
		}

		// Trying to re-capture the ko too soon
		if (position == this.ko_position && this.moves.size() <= this.ko_last_move+1 && this.ko_last_take != move_color) throw new KoViolationException(board,position);
		
		// Trying to play on an occupied intersection
		if (!board.isFree(position)) throw new AlreadyOccupiedException (board,position);

		
		LinkedList<Integer> neighbours = board.neighbours(position);
		//Is it a capturing move ?
		int number_of_non_opponent_surrouding_stones = 0;
		int stones_captured = 0;
		int captured = -1;
		for(int n : neighbours) {
			GoGroup g = groups_by_intersection[n];
			try {
				if (g != null && g.getColor() == GoBoard.reverseColor(move_color) ) {
					if (g.getLibertiesCount() == 1) {
						// This move captures g
						stones_captured += this.capture(g);
						number_of_non_opponent_surrouding_stones++;
						captured = n;
					} 
				} else {
					number_of_non_opponent_surrouding_stones++;
				}
			} catch (UnknownColorException e) {
				System.err.println("Internal error");
				e.printStackTrace();
			}
		}
		// This move does not capture any stone, and is surrounded by 4 enemy stones : it's a suicide move
		if (number_of_non_opponent_surrouding_stones == 0) throw new SuicideMoveException(board,position);
		
		// Is it a ko ?
		if (number_of_non_opponent_surrouding_stones == 1 && stones_captured == 1) {
			ko_last_stone = position;
			ko_position = captured;
			ko_last_move = moves.size()+1;
			ko_last_take = move_color;
		}
		
		int max_liberty_of_own_neighbours_groups = 0;
		int free_space = 0;
		for (int n : neighbours) {
			if (board.getIntersectionColor(n) == move_color) {
				max_liberty_of_own_neighbours_groups = Math.max(max_liberty_of_own_neighbours_groups, groups_by_intersection[n].getLibertiesCount());
			} else if (board.isFree(n)) {
				free_space++;
			}
		}
		
		// Isolated stone
		if (max_liberty_of_own_neighbours_groups == 0) {
			// Isolated stone
			GoGroup stonegroup= new GoGroup(move_color,position,this);
			groups_by_intersection[position] = stonegroup;
			groups_list.add(stonegroup);
			for (int n : neighbours) {
				if (board.getIntersectionColor(n) == opponent_color) {
					GoGroup g = groups_by_intersection[n];
					g.deleteLiberty(position);
				}
			}
		}
		
		// Suicide connection : this stone is connecting groups which all have only one liberty
		if (max_liberty_of_own_neighbours_groups == 1 && free_space == 0 && stones_captured == 0) {
			throw new SuicideMoveException(board,position);
		}
		
		
		// Adding the stone won't commit suicide
		if (max_liberty_of_own_neighbours_groups >= 2 || (max_liberty_of_own_neighbours_groups == 1 && free_space >0)) {
			GoGroup reunion = null;
			for (int n : neighbours) {
				if (board.getIntersectionColor(n) == move_color) {
					if (reunion == null) {
						reunion = groups_by_intersection[n];
						groups_list.remove(reunion);
					} else {
						// This move is connecting some groups
						reunion = GoGroup.merge(reunion, groups_by_intersection[n], this);
						groups_list.remove(groups_by_intersection[n]);
						for (int stone : reunion.getStonesPositions()) groups_by_intersection[stone] = reunion;
					}
				} else if (board.getIntersectionColor(n) == opponent_color) {
					GoGroup g = groups_by_intersection[n];
					g.deleteLiberty(position);
				}
			}
			groups_list.add(reunion);
			reunion.addStone(position);
			groups_by_intersection[position] = reunion;
		}
		
		try {
			board.placeStone(move_color, position);
		} catch (UnknownColorException e) {
			System.err.println("Internal error");
			e.printStackTrace();
		}
		
		long new_hash = board.getHash();
		
		if (this.hash_list.contains(new_hash)){
			throw new SuperKoViolationException(this.getBoard(),position);
		}
		else {
			this.hash_list.removeFirst();
			this.hash_list.add(new_hash);
		}
		
		moves.add(position);
		color_to_play = opponent_color;
		
		//Check if the ko is still here
		if (ko_position != -1) {
			if (ko_position == position) {
				// The ko has been filled
				this.deleteKo();
			} else {
				int libs = groups_by_intersection[this.ko_last_stone].getLibertiesCount();
				if (libs > 1) deleteKo();
			}
		}
	}
	
	private void deleteKo(){
		this.ko_last_move = -1;
		this.ko_last_stone = -1;
		this.ko_last_take = -1;
		this.ko_position = -1;
	}
	
	private int capture(GoGroup g) {
		LinkedList<Integer> stones = g.getStonesPositions();

		try {
			for (int s : stones) {
				board.deleteStone(s);
				groups_by_intersection[s] = null;
				LinkedList<Integer> neighbours = board.neighbours(s);
				for (int n : neighbours) {
					if (groups_by_intersection[n] != null) groups_by_intersection[n].addLiberty(s);
				}
			}
			
		} catch(Exception e) {
			System.err.println("Internal error");
			e.printStackTrace();
			return 0;
		}

		groups_list.remove(g);

		if (g.getColor() == BLACK) black_prisoners += g.getSize();
		else white_prisoners += g.getSize();
		
		return g.getSize();
	}
	
//	public LinkedList<Integer> getLegalMoves() {
//		// Should not be called too much ; kind of heavy
//		Set<Integer> free_intersections = board.getFreeIntersections();
//		LinkedList<Integer> legal_moves = new LinkedList<Integer>();
//		
//		for (int move : free_intersections) {
//			GoGame copy = new GoGame(this);
//			try{
//				copy.move(move);
//				legal_moves.add(move);
//			} catch (IllegalMoveException e) {
//				// Illegal move ; don't add it in the legal moves list
//			} catch (ImpossibleMoveException e) {
//				System.err.println("Internal error");
//				e.printStackTrace();
//			}
//		}
//		
//		return legal_moves;
//	}
	
	public boolean isBlackWinning() {
		// The game must be over when calling this function
		return ((this.black_prisoners + this.komi) < (this.white_prisoners));
	}
	
	public String printInfo(String prefix) {
		String res = board.toString();
		res += "Next move : "+(moves.size()+1)+"\n";
		if (ko_position != -1){
			res += "There is a ko at "+board.labelPosition(ko_position)+" (last to take : "+ko_last_take+" at move "+ko_last_move+")\n";
		}
		res += "White dead stones : "+white_prisoners+", Black dead stones : "+black_prisoners+"\n";
		return res;
	}

}
